// Copyright (c) 2021 Presto Labs Pte. Ltd.
// Author: daniel

#include "coin2/exchange/gateio/api_order/native_private_client.h"

#include <memory>
#include <sstream>

#include <rapidjson/writer.h>

#include "coin2/exchange/gateio/api_util/auth_key.h"
#include "coin2/exchange/gateio/api_util/rate_tracker.h"
#include "coin2/exchange/gateio/symbology/product.h"

using coin2::exchange::gateio::api_order::GateioOrderSubTopicId;
using coin2::exchange::gateio::api_util::GateioRateTracker;
using coin2::exchange::gateio::symbology::GateioProduct;
using AsioHttpContextList = std::list<std::shared_ptr<AsioHttpContext>>;
GateioNativePrivateClient::GateioNativePrivateClient(
    const MarketExchangeApi& mea,
    const NativeClientConfig& config) {
  mea_ = mea;
  NativeClientConfig cfg;
  cfg.set_rest_host("api.gateio.ws");
  cfg.set_rest_host_port(443);
  cfg.MergeFrom(config);
  LOG(INFO) << "[GateioNativePrivateClient] config: " << cfg.DebugString();

  auths_.Construct<GateioRestAuth, NativeClientConfig>(cfg);

  rate_tracker_ = std::make_unique<GateioRateTracker>(mea_, config);
  start_time_ = GetCurrentTimestamp();
}

auto GateioNativePrivateClient::QueryAccountBalance() -> HttpContextPtr {
  auto ts = GetCurrentTimestamp();
  if (!HasEnoughRateLimit(ts, ActionType::QUERY)) {
    LOG(ERROR) << "[QueryAccountBalance] Hit rate limit";
    api_block_info_[__func__]++;
    return {};
  }

  UrlParamsBuilder builder;
  auto context = GetAuth()->CreateRequestByGetWithSignature(path_query_account_balance_, &builder);
  Push(1, ts, ActionType::QUERY);
  api_req_info_[__func__]++;

  context->query_type = OrderSubTopicId::REST_ACCOUNT_BALANCE;
  context->extra_info = DebugString();
  return HttpContextPtr(context);
}

auto GateioNativePrivateClient::QueryOpenOrders(const RestParams& params) -> HttpContextPtr {
  auto ts = GetCurrentTimestamp();
  if (!HasEnoughRateLimit(ts, ActionType::QUERY)) {
    LOG(ERROR) << "[QueryOpenOrders] Hit rate limit";
    api_block_info_[__func__]++;
    return {};
  }

  UrlParamsBuilder builder;
  builder.PutGet("page", params.page());
  builder.PutGet("limit", "100");
  auto context = GetAuth()->CreateRequestByGetWithSignature(path_query_open_orders_, &builder);
  Push(1, ts, ActionType::QUERY);
  api_req_info_[__func__]++;

  if (std::string_view("1") == params.page()) {
    context->query_type = OrderSubTopicId::REST_OPEN_ORDERS;
  } else {
    context->query_type = OrderSubTopicId::REST_ALL_ORDERS;
  }
  context->extra_info = DebugString();
  return HttpContextPtr(context);
}

auto GateioNativePrivateClient::QueryHistOrders(const RestParams& params) -> HttpContextPtr {
  auto ts = GetCurrentTimestamp();
  if (!HasEnoughRateLimit(ts, ActionType::QUERY)) {
    LOG(ERROR) << "[QueryHistOrders] Hit rate limit";
    api_block_info_[__func__]++;
    return {};
  }

  UrlParamsBuilder builder;
  builder.PutGet("currency_pair", params.currency_pair());
  builder.PutGet("status", "finished");
  auto context = GetAuth()->CreateRequestByGetWithSignature(path_query_hist_orders_, &builder);
  Push(1, ts, ActionType::QUERY);
  api_req_info_[__func__]++;

  context->query_type = OrderSubTopicId::REST_DONE_ORDERS;
  context->extra_info = DebugString();
  return HttpContextPtr(context);
}

auto GateioNativePrivateClient::QueryHistTrades(const RestParams& params) -> HttpContextPtr {
  auto ts = GetCurrentTimestamp();
  if (!HasEnoughRateLimit(ts, ActionType::QUERY)) {
    LOG(ERROR) << "[QueryHistTrades] Hit rate limit";
    api_block_info_[__func__]++;
    return {};
  }

  UrlParamsBuilder builder;
  const auto& native_symbol = params.currency_pair();
  builder.PutGet("currency_pair", native_symbol);
  auto context = GetAuth()->CreateRequestByGetWithSignature(path_query_hist_trades_, &builder);
  Push(1, ts, ActionType::QUERY);
  api_req_info_[__func__]++;

  context->query_type = OrderSubTopicId::REST_FILL_ORDERS;
  context->extra_info = DebugString();
  return HttpContextPtr(context);
}

std::string GateioNativePrivateClient::DebugString() const {
  double delta_t = (GetCurrentTimestamp() - start_time_) / 1.0e9;
  std::stringstream ss;
  ss << "GateioNativePrivateClient::DebugString\n";
  ss << "api info\n";
  for (auto& [name, value] : api_req_info_) {
    int32_t blocked = 0;
    if (api_block_info_.count(name) > 0) {
      blocked = api_block_info_.at(name);
    }
    int32_t total_req = value + blocked;
    ss << name << " (total:" << total_req << ", succeeded: " << value << ", blocked: " << blocked
       << ", req_rate: " << value / delta_t << ", block_rate: " << blocked / delta_t << ")\n";
  }
  return ss.str();
}

auto GateioNativePrivateClient::PlaceSingleOrder(const RestParams& params) -> HttpContextPtr {
  auto ts = GetCurrentTimestamp();
  if (!HasEnoughRateLimit(ts, ActionType::PLACE)) {
    LOG(ERROR) << "[" << __func__ << "] Hit rate limit";
    api_block_info_[__func__]++;
    return {};
  }

  UrlParamsBuilder builder;
  builder.PutPost("currency_pair", params.currency_pair());
  builder.PutPost("type", params.type());
  builder.PutPost("side", params.side());
  builder.PutPost("price", params.price());
  builder.PutPost("amount", params.amount());
  builder.PutPost("time_in_force", params.time_in_force());
  builder.PutPost("text", params.text());
  auto context = GetAuth()->CreateRequestByPostWithSignature(path_place_single_order_, &builder);
  Push(1, ts, ActionType::PLACE);
  api_req_info_[__func__]++;

  context->query_type = OrderSubTopicId::SUBMIT_RESPONSE;
  context->extra_info = DebugString();
  return HttpContextPtr(context);
}

auto GateioNativePrivateClient::PlaceBatchOrders(const std::vector<RestParams>& params)
    -> HttpContextPtr {
  auto ts = GetCurrentTimestamp();
  if (!HasEnoughRateLimit(ts, ActionType::PLACE)) {
    LOG(ERROR) << "[" << __func__ << "] Hit rate limit";
    api_block_info_[__func__]++;
    return {};
  }

  rapidjson::StringBuffer str_buf;
  rapidjson::Writer<rapidjson::StringBuffer> writer(str_buf);
  writer.StartArray();

  for (const auto& param : params) {
    writer.StartObject();

    writer.Key("currency_pair");
    writer.String(param.currency_pair().c_str());
    writer.Key("type");
    writer.String(param.type().c_str());
    writer.Key("side");
    writer.String(param.side().c_str());
    writer.Key("price");
    writer.String(param.price().c_str());
    writer.Key("amount");
    writer.String(param.amount().c_str());
    writer.Key("time_in_force");
    writer.String(param.time_in_force().c_str());
    writer.Key("text");
    writer.String(param.text().c_str());

    writer.EndObject();
  }

  writer.EndArray();

  auto context = static_cast<GateioRestAuth*>(GetAuth())->CreateRequestByPostWithSignature(
      path_place_batch_orders_,
      str_buf.GetString());
  Push(1, ts, ActionType::PLACE);
  api_req_info_[__func__]++;

  context->query_type = OrderSubTopicId::MULTI_SUBMIT_RESPONSE;
  context->extra_info = DebugString();
  return HttpContextPtr(context);
}

auto GateioNativePrivateClient::CancelSingleOrder(const RestParams& params) -> HttpContextPtr {
  auto ts = GetCurrentTimestamp();
  if (!HasEnoughRateLimit(ts, ActionType::CANCEL)) {
    LOG(ERROR) << "[" << __func__ << "] Hit rate limit";
    api_block_info_[__func__]++;
    return {};
  }

  UrlParamsBuilder builder;
  builder.PutGet("currency_pair", params.currency_pair());
  std::string path = fmt::format("{}/{}", path_cancel_single_order_, params.order_id());
  auto context = GetAuth()->CreateRequestByDeleteWithSignature(path, &builder);

  Push(1, ts, ActionType::CANCEL);
  api_req_info_[__func__]++;

  context->query_type = OrderSubTopicId::CANCEL_RESPONSE;
  context->extra_info = DebugString();
  return HttpContextPtr(context);
}

auto GateioNativePrivateClient::CancelBatchOrders(const std::vector<RestParams>& params)
    -> HttpContextPtr {
  auto ts = GetCurrentTimestamp();
  if (!HasEnoughRateLimit(ts, ActionType::CANCEL)) {
    LOG(ERROR) << "[" << __func__ << "] Hit rate limit";
    api_block_info_[__func__]++;
    return {};
  }

  rapidjson::StringBuffer str_buf;
  rapidjson::Writer<rapidjson::StringBuffer> writer(str_buf);
  writer.StartArray();

  for (const auto& param : params) {
    writer.StartObject();

    writer.Key("currency_pair");
    writer.String(param.currency_pair().c_str());
    writer.Key("id");
    writer.String(param.order_id().c_str());

    writer.EndObject();
  }

  writer.EndArray();

  auto context = static_cast<GateioRestAuth*>(GetAuth())->CreateRequestByPostWithSignature(
      path_cancel_batch_orders_,
      str_buf.GetString());
  Push(1, ts, ActionType::CANCEL);
  api_req_info_[__func__]++;

  context->query_type = OrderSubTopicId::MULTI_CANCEL_RESPONSE;
  context->extra_info = DebugString();
  return HttpContextPtr(context);
}

auto GateioNativePrivateClient::PlaceOrderNorm(const OrderExecution& order_exe) -> HttpContextPtr {
  if (!order_exe.is_place_batch_orders) {
    const auto& order_spec = *order_exe.order_spec;
    const auto proc_order_id = order_exe.proc_order_id;
    auto params = GetRestOrderParams(order_spec, proc_order_id);
    return PlaceSingleOrder(params);
  }

  auto mutable_order_exe = const_cast<OrderExecution*>(&order_exe);

  if (mutable_order_exe->place_pending_order_deque.empty()) {
    return {};
  }

  OrderExecution::OrderSpecList temp_place_pending_order_deque;
  std::vector<RestParams> params;
  std::unordered_set<std::string> symbol_set;
  int cnt = 0;
  while (!mutable_order_exe->place_pending_order_deque.empty()) {
    const auto* order_spec = mutable_order_exe->place_pending_order_deque.front();
    mutable_order_exe->place_pending_order_deque.pop_front();

    if (symbol_set.size() < 4 && symbol_set.count(order_spec->product) == 0) {
      symbol_set.emplace(order_spec->product);
    }

    if (symbol_set.count(order_spec->product) == 0 || cnt >= 10) {
      temp_place_pending_order_deque.emplace_back(order_spec);
      continue;
    }

    mutable_order_exe->place_proceed_order_deque.emplace_back(order_spec);
    ++cnt;

    params.emplace_back(GetRestOrderParams(*order_spec, order_spec->proc_order_id));
  }

  mutable_order_exe->place_pending_order_deque = std::move(temp_place_pending_order_deque);

  return PlaceBatchOrders(params);
}

auto GateioNativePrivateClient::CancelOrderNorm(const OrderExecution& order_exe) -> HttpContextPtr {
  if (!order_exe.is_cancel_batch_orders) {
    auto params = GetRestCancelParams(*order_exe.oc);
    return CancelSingleOrder(params);
  }

  auto mutable_order_exe = const_cast<OrderExecution*>(&order_exe);

  if (mutable_order_exe->cancel_pending_order_deque.empty()) {
    return {};
  }

  std::vector<RestParams> params;
  for (int i = 0; i < 20 && !mutable_order_exe->cancel_pending_order_deque.empty(); ++i) {
    const auto* order_context = mutable_order_exe->cancel_pending_order_deque.front();
    mutable_order_exe->cancel_pending_order_deque.pop_front();

    mutable_order_exe->cancel_proceed_order_deque.push_back(order_context);

    RestParams param = GetRestCancelParams(*order_context);

    params.emplace_back(std::move(param));
  }

  return CancelBatchOrders(params);
}

auto GateioNativePrivateClient::QueryDeposit(const RestParams& params) -> HttpContextPtr {
  auto ts = GetCurrentTimestamp();
  if (!HasEnoughRateLimit(ts, ActionType::QUERY)) {
    LOG(ERROR) << "[QueryDeposit] Hit rate limit";
    api_block_info_[__func__]++;
    return {};
  }

  UrlParamsBuilder builder;
  builder.PutGet("limit", 1000);
  if (params.has_start_time() && params.has_end_time()) {
    builder.PutGet("from", params.start_time());
    builder.PutGet("to", params.end_time());
  }
  auto context = GetAuth()->CreateRequestByGetWithSignature(path_query_deposit_, &builder);
  Push(1, ts, ActionType::QUERY);
  api_req_info_[__func__]++;

  context->query_type = GateioOrderSubTopicId::REST_DEPOSIT;
  return HttpContextPtr(context);
}

auto GateioNativePrivateClient::QueryWithdrawal(const RestParams& params) -> HttpContextPtr {
  auto ts = GetCurrentTimestamp();
  if (!HasEnoughRateLimit(ts, ActionType::QUERY)) {
    LOG(ERROR) << "[QueryWithdrawal] Hit rate limit";
    api_block_info_[__func__]++;
    return {};
  }

  UrlParamsBuilder builder;
  builder.PutGet("limit", 1000);
  if (params.has_start_time() && params.has_end_time()) {
    builder.PutGet("from", params.start_time());
    builder.PutGet("to", params.end_time());
  }
  auto context = GetAuth()->CreateRequestByGetWithSignature(path_query_withdrawal_, &builder);
  Push(1, ts, ActionType::QUERY);
  api_req_info_[__func__]++;

  context->query_type = GateioOrderSubTopicId::REST_WITHDRAWAL;
  return HttpContextPtr(context);
}

auto GateioNativePrivateClient::QueryTransfers(const RestParams& params) -> HttpContextPtr {
  auto ts = GetCurrentTimestamp();
  if (!HasEnoughRateLimit(ts, ActionType::QUERY)) {
    LOG(ERROR) << "[QueryTransfers] Hit rate limit";
    api_block_info_[__func__]++;
    return {};
  }

  UrlParamsBuilder builder;
  builder.PutGet("limit", 1000);
  if (params.has_start_time() && params.has_end_time()) {
    builder.PutGet("from", params.start_time());
    builder.PutGet("to", params.end_time());
  }

  auto context = GetAuth()->CreateRequestByGetWithSignature(path_query_transfers, &builder);
  Push(1, ts, ActionType::QUERY);
  api_req_info_[__func__]++;

  context->query_type = GateioOrderSubTopicId::REST_TRANSFERS;
  return HttpContextPtr(context);
}

auto GateioNativePrivateClient::GetRestOrderParams(
    const OrderSpec& order_spec,
    int64_t proc_order_id) -> RestParams {
  RestParams params;
  params.set_currency_pair(order_spec.product);
  if (order_spec.order_type == OrderType::LIMIT_ORDER) {
    auto product = GateioProduct::FromStr(order_spec.product, order_spec.order_created_time);
    auto product_holder = GetProductHolder(product, order_spec.order_created_time);
    auto formatter = product_holder->product_info().order_formatter();
    params.set_price(formatter.FormatPrice(order_spec.price));
    params.set_amount(formatter.FormatQty(order_spec.qty));
  } else {
    NOTREACHED() << "unsupported order type: " << static_cast<int>(order_spec.order_type);
  }
  if (order_spec.order_side == OrderSide::BUY_ORDER) {
    params.set_side("buy");
  } else if (order_spec.order_side == OrderSide::SELL_ORDER) {
    params.set_side("sell");
  } else {
    NOTREACHED() << "Unknown order side: " << static_cast<int>(order_spec.order_side);
  }
  params.set_type("limit");
  if (order_spec.post_only) {
    params.set_time_in_force("poc");
  } else if (order_spec.duration == coin::proto::OrderDuration::IOC_ORDER) {
    params.set_time_in_force("ioc");
  } else {
    params.set_time_in_force("gtc");
  }
  params.set_text(fmt::format("t-{}", proc_order_id));

  return params;
}

auto GateioNativePrivateClient::GetRestCancelParams(const IOrderContext& order_context)
    -> RestParams {
  RestParams params;
  params.set_order_id(order_context.exchange_order_id());
  params.set_currency_pair(order_context.native_product());

  return params;
}

bool GateioNativePrivateClient::IsMasterAccount() const {
  auto auth = static_cast<GateioRestAuth*>(GetAuth());
  std::string account_type = auth->GetAccountType();
  if (account_type == std::string("master")) {
    return true;
  } else {
    return false;
  }
}

std::shared_ptr<AsioHttpContext> GateioNativePrivateClient::SubmitOrderExecution(
    const OrderExecutionProto& order) {
  switch (order.type()) {
    case OrderExecutionProto::SUBMIT_ORDER: {
      auto order_spec = OrderSpec::FromProto(order.order_spec());
      auto params = GetRestOrderParams(*order_spec, order.proc_order_id());
      return PlaceSingleOrder(params);
    }
    case OrderExecutionProto::CANCEL_ORDER: {
      auto order_spec = OrderSpec::FromProto(order.order_spec());
      auto params = GetRestOrderParams(*order_spec, order.proc_order_id());
      return CancelSingleOrder(params);
    }
    default: {
      return nullptr;
    }
  }
}

std::shared_ptr<AsioHttpContext> GateioNativePrivateClient::SubmitRestQuery(
    const RestQueryProto& query) {
  RestParams params;
  switch (query.type()) {
    case RestQueryProto::QUERY_ACCOUNT_BALANCE: {
      return QueryAccountBalance();
    }
    case RestQueryProto::QUERY_OPEN_ORDERS: {
      RestParams params;
      params.set_page("1");
      return QueryOpenOrders(params);
    }
    default: {
      return nullptr;
    }
  }
}

AsioHttpContextList GateioNativePrivateClient::SubmitRestQueryList(const RestQueryProto& query) {
  AsioHttpContextList list;
  RestParams params;
  auto fill_func = [&params](int64_t start_time, int64_t end_time) {
    params.set_start_time(start_time / 1000'000'000);
    params.set_end_time(end_time / 1000'000'000);
  };

  switch (query.type()) {
    case RestQueryProto::QUERY_ACCOUNT_BALANCE: {
      list.emplace_back(QueryAccountBalance());
      break;
    }
    case RestQueryProto::QUERY_OPEN_ORDERS: {
      params.set_page("1");
      list.emplace_back(QueryOpenOrders(params));
      break;
    }
    case RestQueryProto::QUERY_TRANSFERS: {
      fill_func(query.start_time(), query.end_time());
      list.emplace_back(QueryTransfers(params));
      break;
    }
    case RestQueryProto::QUERY_WITHDRAW: {
      fill_func(query.start_time(), query.end_time());
      list.emplace_back(QueryWithdrawal(params));
      break;
    }
    case RestQueryProto::QUERY_DEPOSIT: {
      fill_func(query.start_time(), query.end_time());
      list.emplace_back(QueryDeposit(params));
      break;
    }

    default: {
      return list;
    }
  }

  return list;
}
